/**
 * 
 */
package za.co.sindi.oauth.server.config.builder;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import za.co.sindi.oauth.core.annotation.Path;
import za.co.sindi.oauth.core.annotation.Request;
import za.co.sindi.oauth.core.annotation.Service;
import za.co.sindi.oauth.core.annotation.enums.RequestMethod;
import za.co.sindi.oauth.core.scanner.ClassScanner;
import za.co.sindi.oauth.exception.ConfigBuilderException;
import za.co.sindi.oauth.exception.ScanningException;
import za.co.sindi.oauth.server.config.ConfigBuilder;
import za.co.sindi.oauth.server.config.ModuleConfig;
import za.co.sindi.oauth.server.config.RequestConfig;
import za.co.sindi.oauth.server.config.ServiceConfig;

/**
 * @author Buhake Sindi
 * @since 12 April 2012
 *
 */
public class DefaultConfigBuilder implements ConfigBuilder {

	private static final Logger logger = Logger.getLogger(DefaultConfigBuilder.class);
	private static final String TAG_OAUTH_CONFIG = "oauth-config";
	private static final String TAG_SERVICES = "services";
	private static final String TAG_SERVICE = "service";
	private static final String TAG_REQUEST = "request";
	
	private ClassScanner scanner;
	private String basePackageName;
	
	/* (non-Javadoc)
	 * @see com.neurologic.oauth.server.config.ConfigBuilder#setClassScanner(com.neurologic.oauth.core.scanner.ClassScanner)
	 */
	@Override
	public void setClassScanner(ClassScanner scanner) {
		// TODO Auto-generated method stub
		this.scanner = scanner;
	}

	/* (non-Javadoc)
	 * @see com.neurologic.oauth.server.config.ConfigBuilder#setBasePackageName(java.lang.String)
	 */
	@Override
	public void setBasePackageName(String basePackageName) {
		// TODO Auto-generated method stub
		this.basePackageName = basePackageName;
	}

	/* (non-Javadoc)
	 * @see com.neurologic.oauth.server.config.ConfigBuilder#build(java.net.URL)
	 */
	@Override
	public ModuleConfig build(URL resource) throws ConfigBuilderException {
		// TODO Auto-generated method stub
		if (resource == null) {
			throw new IllegalArgumentException("URL resource may not be null.");
		}
		
		try {
			return build(resource.openStream());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			throw new ConfigBuilderException("parse()", e);
		}
	}

	/* (non-Javadoc)
	 * @see com.neurologic.oauth.server.config.ConfigBuilder#build(java.io.InputStream)
	 */
	@Override
	public ModuleConfig build(InputStream input) throws ConfigBuilderException {
		// TODO Auto-generated method stub
		ModuleConfig config = null;
		
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			InputSource iSource = new InputSource(input);
			Element rootElement = builder.parse(iSource).getDocumentElement();
			
			//This MUST exist, even if there are no sub-elements
			if (!TAG_OAUTH_CONFIG.equals(rootElement.getTagName())) {
				throw new ConfigBuilderException("Unknown tag name '" + rootElement.getTagName() + "'. Expected '" + TAG_OAUTH_CONFIG + "'.");
			}
			
			//Instantiate ModuleConfig
			config = new ModuleConfig();
			
			//Get NodeList
			NodeList nodes = rootElement.getElementsByTagName(TAG_SERVICES);
			if (nodes != null && nodes.getLength() > 0) {
//				throw new ConfigBuilderException("Expected tag '" + TAG_SERVICES + "'.");
				
				if (nodes.getLength() != 1) {
					throw new ConfigBuilderException("There can only be one '" + TAG_SERVICES +"' tag (we found " + nodes.getLength() + ").");
				}
				
				//Iterate through service
				nodes = ((Element)nodes.item(0)).getElementsByTagName(TAG_SERVICE);
				if (nodes != null) {
					for (int i = 0; i < nodes.getLength(); i++) {
						ServiceConfig serviceConfig = createServiceConfig((Element) nodes.item(i));
						if (config.serviceConfigExistsByPath(serviceConfig.getPath())) {
							throw new ConfigBuilderException("Duplicate service (" + serviceConfig.getName() + ") found with path '" + serviceConfig.getPath() + "'.");
						}
						
						config.addServiceConfig(serviceConfig);
					}
				}
			}
			
			//Scan classes too
			scan(config);
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			throw new ConfigBuilderException("parse()", e);
		} catch (SAXException e) {
			// TODO Auto-generated catch block
			throw new ConfigBuilderException("parse()", e);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			throw new ConfigBuilderException("parse()", e);
		} catch (ScanningException e) {
			// TODO Auto-generated catch block
			throw new ConfigBuilderException("parse()", e);
		}
		
		return config;
	}
	
	private ServiceConfig createServiceConfig(Element element) throws ConfigBuilderException {
		ServiceConfig serviceConfig = new ServiceConfig();
		
		String path = element.getAttribute("path");
		if (path == null || path.isEmpty()) {
			throw new ConfigBuilderException("Element '" + element.getTagName() + "' may not contain an empty or null 'path' attribute.");
		}
		
		String className = element.getAttribute("class");
		if (className == null || className.isEmpty()) {
			throw new ConfigBuilderException("Element '" + element.getTagName() + "' may not contain an empty or null 'class' attribute.");
		}
		
		String name = element.getAttribute("name");
		if (name == null || name.isEmpty()) {
			name = className.substring(className.lastIndexOf('.') + 1);
			name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
			
			if (logger.isInfoEnabled()) {
				logger.info("@Service contains no name value set, setting name as '" + name + "'.");
			}
		}
		
		serviceConfig.setName(name);
		serviceConfig.setPath(path);
		serviceConfig.setClassName(className);
		serviceConfig.setAnnotated(false);
		serviceConfig.setServiceSubclass(true);
		
		NodeList requests = element.getElementsByTagName(TAG_REQUEST);
		if (requests != null) {
			for (int i = 0; i < requests.getLength(); i++) {
				serviceConfig.addRequestConfig(createRequestConfig((Element) requests.item(i)));
			}
		}
		
		return serviceConfig;
	}
	
	private RequestConfig createRequestConfig(Element element) {
		RequestConfig requestConfig = new RequestConfig();
		requestConfig.setMethod(element.getAttribute("method"));
		requestConfig.setExecute(element.getAttribute("execute"));
		
		return requestConfig;
	}
	
	private void scan(ModuleConfig config) throws ScanningException, ConfigBuilderException {
		if (scanner != null) {
			Set<Class<?>> classes = scanner.scan(basePackageName);
			if (classes != null) {
				for (Class<?> _class : classes) {
					Service serviceAnnotation = _class.getAnnotation(Service.class);
					boolean serviceSubclass = za.co.sindi.oauth.core.service.Service.class.isAssignableFrom(_class);
					if (serviceAnnotation != null || serviceSubclass) {
						Path pathAnnotation = _class.getAnnotation(Path.class);
						if (pathAnnotation == null) {
							throw new ConfigBuilderException("@Path annotation not defined.");
						}
						
						String path = pathAnnotation.value();
						if (path == null || path.isEmpty()) {
							throw new ConfigBuilderException("No path attribute provided.");
						}
						
						if (!path.startsWith("/")) {
							throw new ConfigBuilderException("Path doesn't start with '/'.");
						}
						
						ServiceConfig serviceConfig = config.getServiceConfigByPath(path);
						if (serviceConfig == null) {
							serviceConfig = createServiceConfig(serviceAnnotation, pathAnnotation, _class.getName());
						} else {
							if (logger.isInfoEnabled()) {
								logger.info("Service (name=" + serviceConfig.getName() + ", class=" + serviceConfig.getClassName() + ") already exists.");
							}
						}
						serviceConfig.setServiceSubclass(serviceSubclass);
						
						//Get all methods
						Method[] methods = _class.getMethods();
						if (methods != null) {
							for (Method method : methods) {
								Request requestAnnotation = method.getAnnotation(Request.class);
								if (requestAnnotation != null) {
									for (RequestMethod requestMethod : requestAnnotation.value()) {
										serviceConfig.addRequestConfig(createRequestConfig(requestMethod, method.getName()));
									}
								}
							}
						}
						
//						if (config.serviceConfigExistsByPath(serviceConfig.getPath())) {
//							throw new ConfigBuilderException("Duplicate service (" + serviceConfig.getName() + ") found with path '" + serviceConfig.getPath() + "'.");
//						}
						
						if (!config.serviceConfigExistsByPath(path)) {
							if (logger.isInfoEnabled()) {
								logger.info("Adding Service (name=" + serviceConfig.getName() + ", class=" + serviceConfig.getClassName() + ").");
							}
							
							config.addServiceConfig(serviceConfig);
						}
					}
				}
			}
		}
	}
	
	private ServiceConfig createServiceConfig(Service serviceAnnotation, Path pathAnnotation, String className) throws ConfigBuilderException {
		ServiceConfig serviceConfig = new ServiceConfig();
		String name = serviceAnnotation != null ? serviceAnnotation.value() : null;
		if (name == null || name.isEmpty()) {
			name = className.substring(className.lastIndexOf('.') + 1);
			name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
			
			if (logger.isInfoEnabled()) {
				logger.info("Service contains no name, setting name as '" + name + "'.");
			}
		}
		
		serviceConfig.setName(name);
		serviceConfig.setPath(pathAnnotation.value());
		serviceConfig.setClassName(className);
		serviceConfig.setAnnotated((serviceAnnotation != null));
		
		return serviceConfig;
	}
	
	private RequestConfig createRequestConfig(RequestMethod requestMethod, String methodName) {
		RequestConfig requestConfig = new RequestConfig();
		requestConfig.setMethod(requestMethod.toString());
		requestConfig.setExecute(methodName);
		
		return requestConfig;
	}
}
